Skip to content

Improve error UX when chat proposal parsing fails#582

Merged
Chris0Jeky merged 9 commits intomainfrom
enhance/572-chat-error-ux
Mar 29, 2026
Merged

Improve error UX when chat proposal parsing fails#582
Chris0Jeky merged 9 commits intomainfrom
enhance/572-chat-error-ux

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • Fixes Better error UX when chat proposal parsing fails #572
  • Backend: Replace flat parse-failure error string with structured ParseHintPayload containing supportedPatterns array, detectedIntent, closestPattern, and exampleInstruction. Uses keyword scoring to suggest the most relevant pattern.
  • Backend: ChatService detects structured parse hints and sets messageType to parse-hint for frontend rendering. Non-hint errors still use the existing status type (backward compatible).
  • Frontend: Render parse-hint messages as a styled info card (not error-style) with detected intent display, closest pattern suggestion, "Try this instead" button that pre-fills the chat input, and a collapsible list of all supported patterns.

Test plan

  • Parse failure shows formatted hint card instead of raw error
  • "Try this instead" button pre-fills chat input with suggested format
  • Hint is contextually relevant to detected intent
  • Supported patterns list is collapsible
  • Backend returns structured error with supportedPatterns array
  • Backend tests: intent detection, pattern matching, structured message format (8 new tests)
  • Frontend tests: hint card rendering, toggle patterns, pre-fill interaction, extractParseHint utility (8 new tests)
  • All existing tests pass (1606 backend + 1171 frontend)

…atching

Replace flat error string in AutomationPlannerService with a structured
ParseHintPayload containing supportedPatterns array, detectedIntent,
closestPattern, and exampleInstruction. Uses keyword scoring to find the
most relevant pattern suggestion for the user's input.
Detect structured parse hint marker in planner error messages and set
messageType to parse-hint so the frontend can render a hint card instead
of inline error text. Non-hint errors still fall through to status type.
Display parse-hint messages as a styled info card showing detected intent,
closest matching pattern, and a pre-fill button. Include a collapsible
list of all supported patterns. Card uses info styling, not error styling.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Self-Review Findings

API contract

  • No breaking change: The Result.ErrorMessage field is still a string. The structured JSON is appended after a marker within that string. Old frontends that don't understand parse-hint messageType will render the full content as markdown (graceful degradation). The parse-hint messageType is additive to the ChatMessageType union.

XSS risk

  • The hint card renders textBeforeHint through renderMarkdown() which uses DOMPurify. Pattern strings and example instructions are rendered via {{ }} interpolation (not v-html), so no injection vector.
  • The exampleInstruction value from the backend flows into messageContent.value (textarea pre-fill) which is also safe since textareas don't interpret HTML.

Hardcoded strings

  • Pattern strings and intent keywords are English-only and hardcoded in AutomationPlannerService.SupportedPatterns. This matches the existing instruction parsing which is regex-based and English-only. i18n would require a broader refactor beyond this issue's scope.
  • Frontend hint card labels ("Try this instead", "Show all patterns", "Detected intent:") are hardcoded. Consistent with the rest of the codebase which does not use i18n yet.

Accessibility

  • Hint card uses role="region" with aria-label="Instruction format hint".
  • Toggle button has aria-expanded attribute that updates correctly.
  • Patterns list uses role="list" with aria-label.
  • All interactive elements are native <button> elements (keyboard-accessible by default).

Edge cases

  • No detected intent: Handled -- displays "Could not detect intent" and falls back to first pattern.
  • Empty patterns list: Not possible -- SupportedPatterns is a compile-time constant with 12 entries. The extractParseHint utility returns null if supportedPatterns is not an array, and the template falls back to regular markdown rendering.
  • Malformed JSON after marker: extractParseHint catches parse errors and returns null, falling through to normal message rendering.

State management

  • expandedHintIds is a Set<string> keyed by message ID. Creating a new Set on each toggle ensures Vue reactivity. No risk of stale state across sessions since message IDs are unique.
  • applyHintSuggestion sets both messageContent and requestProposal (enabling the proposal checkbox), which is the right UX for a "try this instead" flow.

Potential concern

  • getParseHint() is called multiple times in the template for the same message (header, suggestion, actions, patterns list). This re-parses JSON each time. For the typical message count in a chat session this is negligible, but could be optimized with a computed map if performance becomes an issue. Acceptable for now.

Test coverage

  • Backend: 8 new tests covering DetectIntent, FindClosestPattern, BuildParseHintMessage, and end-to-end ParseInstructionAsync with structured hint.
  • Frontend: 2 new view tests (hint card rendering + pre-fill, toggle patterns), 6 new utility tests for extractParseHint.
  • All 1606 backend tests and 1171 frontend tests pass.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a structured 'parse hint' system to assist users when automation instructions cannot be parsed. The backend now identifies user intent and suggests the closest matching command pattern, returning this data as a JSON payload. The frontend has been updated to render these hints as interactive cards, allowing users to quickly apply suggested formats. Feedback focuses on refactoring the intent detection logic for better maintainability and optimizing the frontend rendering to avoid redundant parsing of the hint data.

I am having trouble creating individual review comments. Click here to see my feedback.

backend/src/Taskdeck.Application/Services/AutomationPlannerService.cs (461-479)

medium

The DetectIntent method uses a series of if statements which can become hard to maintain as more intents are added. Consider refactoring this to use a switch expression for a more modern and declarative approach that is easier to read and extend.

    internal static string? DetectIntent(string instruction)
    {
        var lower = instruction.Trim().ToLowerInvariant();

        return lower switch
        {
            var s when s.Contains("create") || s.Contains("add") || s.Contains("new") => "create",
            var s when s.Contains("move") || s.Contains("drag") || s.Contains("transfer") => "move",
            var s when s.Contains("archive") || s.Contains("remove") || s.Contains("delete") => "archive",
            var s when s.Contains("update") || s.Contains("edit") || s.Contains("change") || s.Contains("rename") || s.Contains("set") => "update",
            var s when s.Contains("unarchive") || s.Contains("restore") => "unarchive",
            var s when s.Contains("reorder") || s.Contains("position") => "reorder",
            _ => null
        };
    }

frontend/taskdeck-web/src/views/AutomationChatView.vue (604-651)

medium

The getParseHint(message) function is called multiple times within the template for each message. This is inefficient as it re-parses the message content on every render cycle. This also forces the use of the non-null assertion operator (!), which can be brittle.

A better approach is to process the messages once. You can create a computed property that memoizes the parsed hint for each message.

In <script setup>:

const messagesWithHints = computed(() =>
  sortedMessages.value.map(message => ({
    ...message,
    parsedHint: getParseHint(message),
  }))
);

In <template>:
Then, iterate over messagesWithHints and use message.parsedHint directly, which will be either the parsed object or null. This avoids repeated parsing and the need for !.

For example:

<div v-for="message in messagesWithHints" ...>
  ...
  <template v-if="message.messageType === 'parse-hint' && message.parsedHint">
    <div
      class="td-message-content td-message-content--markdown"
      v-html="renderMarkdown(message.parsedHint.textBeforeHint)"
    ></div>
    <div class="td-hint-card" ...>
      ...
      {{ message.parsedHint.hint.detectedIntent ? ... }}
      ...
    </div>
  </template>
  ...
</div>

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial Review -- PR #582

Critical

None found.

Major

1. DetectIntent ordering bug: "unarchive" always misclassified as "archive" (AutomationPlannerService.cs:469-474)

The archive check (lower.Contains("archive")) at line 469 fires before unarchive at line 473. Since the string "unarchive" contains the substring "archive", any instruction containing "unarchive" (e.g., "unarchive the board") will be detected as intent "archive" instead of "unarchive". This causes the hint card to suggest archive card {id} rather than unarchive board, which is the opposite of what the user wanted.

The existing test only checks "restore the board" for the unarchive intent, which happens to work because "restore" does not contain "archive". A test for DetectIntent("unarchive my board") expecting "unarchive" would fail.

Fix: Move the unarchive/restore check before the archive/remove/delete check, and use StartsWith("unarchive") or a whole-word check to disambiguate. Similarly, "rename" contains "new" -- an instruction like "rename this card" would be detected as "create" (via "new" in "rename"). Consider checking for longer/more-specific keywords first, or using word-boundary matching.

2. Substring matching in FindClosestPattern causes false keyword hits (AutomationPlannerService.cs:496)

if (words.Any(w => w.Contains(keyword)))

This checks if any word in the instruction contains the keyword as a substring. Short keywords like "set", "new", "in", "to", "add", "desc" will match unintended words:

  • "sunset" matches "set" (boosts "update" patterns)
  • "address" matches "add" (boosts "create" patterns)
  • "into" matches "in" and "to" (boosts "create card in column" and "move" patterns)
  • "description" matches "desc" (correct) but also "new" from "renew" etc.

This inflates scores for wrong patterns. Should use exact word equality (w == keyword or w.Equals(keyword)) instead of w.Contains(keyword).

Minor

3. [PARSE_HINT] marker in LLM-echoed content could suppress hint card

If the LLM echoes a user message containing the literal text [PARSE_HINT], and that message later fails parsing, the assistantContent in ChatService.cs:252 would be "{llmContent}\n\n{hintContext}\n{errorMessage}". The frontend's extractParseHint uses indexOf which finds the first occurrence. If the LLM response includes a spurious [PARSE_HINT], the frontend would try to parse the text after that (not the real payload), fail, and fall back to plain markdown. The real hint card is silently lost.

Mitigation: use lastIndexOf instead of indexOf in the frontend extractParseHint, or use a more unique marker (e.g., a UUID-namespaced marker).

4. No test for DetectIntent("unarchive ...") returning "unarchive"

The test at line 862 only tests "restore the board" for the unarchive intent. Adding [InlineData("unarchive the board", "unarchive")] would reveal Major issue #1. Similarly, "rename board to sprint 5" should test "update" but would currently return "create" (because "rename" contains "new").

5. applyHintSuggestion overwrites user input without confirmation

If the user is typing in the message textarea and clicks "Try this instead", their in-progress text is silently replaced. There is no confirmation dialog or undo mechanism. This is a minor UX concern -- the pre-fill should either warn or only apply when the textarea is empty.

Nits

6. getParseHint called multiple times but is now O(1)

The self-review and Gemini both flagged repeated getParseHint(message) calls in the template. However, the implementation already uses a computed parseHintsByMessageId map, so getParseHint is just a Map.get() lookup -- no re-parsing occurs. The non-null assertion operator ! is still slightly smelly; a local v-if-scoped variable via v-for destructuring or a wrapper component would be cleaner, but this is cosmetic.

7. "set" keyword too broad for "update" intent

The keyword "set" in the update intent check (line 471) is extremely short. An instruction like "reset all cards" or "offset values" would match "update" intent. Consider removing "set" or requiring "set " (with trailing space) as a prefix.

8. CSS color-mix may not be supported in older browsers

.td-hint-card uses color-mix(in srgb, ...) which requires Chrome 111+, Firefox 113+, Safari 16.2+. If the app needs to support older browsers, provide a fallback.

Overall Assessment

Pass with fixes. The unarchive/archive ordering bug (#1) and substring keyword matching (#2) are correctness issues that will cause wrong suggestions in real usage. Both are straightforward fixes. The remaining items are robustness and polish concerns that can be addressed in follow-up.

- Reorder DetectIntent checks so "unarchive" is checked before "archive"
  (previously "unarchive" was always misclassified as "archive" because
  the string contains the substring)
- Switch from substring matching to whole-word matching in both
  DetectIntent and FindClosestPattern to prevent false positives
  (e.g. "sunset" matching "set", "address" matching "add")
- Use lastIndexOf for PARSE_HINT marker in frontend to handle edge case
  where LLM response contains the marker text
- Add regression tests for "unarchive my board" and "rename board"
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Follow-up: Fixes pushed (37f3f49)

Addressed Major issues #1 and #2 from the adversarial review, plus Minor #3:

1. DetectIntent ordering bug (Major #1)

  • Moved unarchive/restore check before archive/remove/delete
  • Switched from string.Contains() to word-level matching (hasWord()) to prevent substring false positives (e.g., "rename" matching "new")
  • Added regression tests: "unarchive my board" -> "unarchive", "rename board to Sprint 5" -> "update"

2. Substring matching in FindClosestPattern (Major #2)

  • Changed w.Contains(keyword) to w == keyword for exact word matching
  • Prevents false boosts from words like "sunset" matching "set", "address" matching "add"

3. PARSE_HINT marker robustness (Minor #3)

  • Changed indexOf to lastIndexOf in frontend extractParseHint so the real marker is found even if earlier content contains the marker text

Verification:

  • All 1608 backend tests pass (45 AutomationPlannerService tests including 2 new regression tests)
  • All 1171 frontend tests pass
  • Frontend build succeeds (typecheck + vite build)

@Chris0Jeky Chris0Jeky merged commit ecc7ef7 into main Mar 29, 2026
18 checks passed
@Chris0Jeky Chris0Jeky deleted the enhance/572-chat-error-ux branch March 29, 2026 22:22
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Mar 29, 2026
Chris0Jeky added a commit that referenced this pull request Mar 29, 2026
Update two analysis docs (chat-to-proposal gap and manual testing findings) to reflect recent fixes and testing status. Key changes: add Last Updated and status notes; mark Tier 1 improvements shipped (intent classifier regex/stemming/negation fixes, substring ordering bug, PR #579), UX parse hints shipped (PR #582), unit/integration tests shipped (PR #580), and note PR range #578#582. In manual testing findings mark OBS-2/OBS-3 resolved (PR #581) and BUG-M5 resolved (PR #578), update resolutions and remove duplicate checklist items. Minor editorial clarifications and test counts added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Better error UX when chat proposal parsing fails

1 participant